let/const 与块级作用域面试题全解析
一、核心要点速览
💡 核心考点
- 块级作用域: {} 内声明的变量只在该区域有效
- 暂时性死区 (TDZ): 声明前访问会报 ReferenceError
- const 本质: 保证绑定关系不变,而非值不变
- 使用场景: 优先使用 const,需要重新赋值时用 let
二、var 的问题与块级作用域
1. var 的变量提升问题
javascript
// var 的问题:变量提升
console.log(a) // undefined(不会报错)
var a = 1
// 等价于:
var a
console.log(a) // undefined
a = 1
// 函数提升也会造成问题
if (false) {
function fn() {
console.log('never call')
}
}
fn() // ❌ 仍然可以调用!2. let/const 的块级作用域
javascript
// let/const: 块级作用域
{
let b = 2
const c = 3
console.log(b) // 2
}
console.log(b) // ReferenceError: b is not defined
console.log(c) // ReferenceError: c is not defined
// for 循环中的应用
for (let i = 0; i < 5; i++) {
console.log(i) // 0, 1, 2, 3, 4
}
console.log(i) // ReferenceError
// var 的情况
for (var j = 0; j < 5; j++) {
console.log(j) // 0, 1, 2, 3, 4
}
console.log(j) // 4(泄漏到外部)3. 经典面试题:setTimeout 与闭包
javascript
// ❌ var 版本
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i) // 5, 5, 5, 5, 5
}, 1000)
}
// 原因:只有一个 i,所有定时器共享
// ✓ let 版本
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i) // 0, 1, 2, 3, 4
}, 1000)
}
// 原因:每次循环都有新的 i
// 等价于:
// { let i = 0; setTimeout(...) }
// { let i = 1; setTimeout(...) }
// ...
// var 版本的解决方案(使用 IIFE)
for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => {
console.log(j) // 0, 1, 2, 3, 4
}, 1000)
})(i)
}三、暂时性死区 (TDZ)
1. TDZ 详解
┌──────────────────────────────────────────────────────────┐
│ 暂时性死区 (Temporal Dead Zone) │
└──────────────────────────────────────────────────────────┘
代码执行流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function example() {
console.log(x) // ❌ ReferenceError
let x = 10
console.log(y) // ❌ ReferenceError
const y = 20
}
执行时序图:
时间 → ─────────────────────────────────────────►
进入作用域
│
▼
┌─────────────────┐
│ 创建 x, y 绑定 │ ← 已声明但未初始化
│ (存在于 TDZ) │
└────────┬────────┘
│
▼
┌─────────┐
│ TDZ 区域 │ ← 访问会报 ReferenceError
│ (禁止访问)│
└─────────┘
│
▼
let x = 10
│
▼
┌─────────┐
│ x 可访问 │ ← 初始化完成
└─────────┘
│
▼
const y = 20
│
▼
┌─────────┐
│ y 可访问 │
└─────────┘
关键点:
✓ let/const 声明的变量在声明前不可访问
✓ TDZ 从作用域开始到变量声明处
✓ typeof 在 TDZ 内也会报错
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━2. TDZ 示例
javascript
// TDZ 示例 1
console.log(typeof x) // ReferenceError
let x = 10
// 如果在 TDZ 之前,typeof 应该返回 'undefined'
// 但在 TDZ 内,typeof 也会报错!
// TDZ 示例 2
function test() {
console.log(a) // undefined(访问的是全局的 a)
if (false) {
let a = 1
}
}
let a = 10
test()
// TDZ 示例 3:默认参数
function fn(x = y, y = 1) {
console.log(x, y)
}
fn() // ReferenceError: y is not defined
// 因为 x 的默认值求值时,y 还在 TDZ 中四、const 的本质
1. const 的特性
javascript
// const 保证的是绑定关系不变,而非值不变
// ✓ 基本类型:值不能变
const a = 1
a = 2 // TypeError: Assignment to constant variable.
// ✓ 引用类型:内存地址不能变,但属性可变
const obj = { name: 'Vue' }
obj.name = 'React' // ✓ 可以
obj.age = 3 // ✓ 可以
obj = {} // ✗ TypeError
// ✓ 数组同理
const arr = [1, 2]
arr.push(3) // ✓ 可以
arr[0] = 100 // ✓ 可以
arr.length = 0 // ✓ 可以
arr = [] // ✗ TypeError
// ✓ 对象冻结(完全不可变)
const frozen = Object.freeze({ name: 'Vue' })
frozen.name = 'React' // ✗ 严格模式下报错2. 深度冻结对象
javascript
// 浅冻结
const obj = { name: 'Vue' }
Object.freeze(obj)
obj.name = 'React' // 无效
// 深冻结
function deepFreeze(obj) {
Object.freeze(obj)
Object.keys(obj).forEach(key => {
const value = obj[key]
if (typeof value === 'object' && value !== null) {
deepFreeze(value)
}
})
}
const nested = {
user: {
name: 'Vue',
skills: ['JS', 'CSS']
}
}
deepFreeze(nested)
nested.user.name = 'React' // 无效
nested.user.skills.push('HTML') // 无效五、实际应用场景
1. 使用场景决策树
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景选择指南:
需要重新赋值?
│
├─ 是 → 使用 let
│ └─ 例如:循环计数器、累加器、状态标志
│
└─ 否 → 使用 const
└─ 90% 的情况应该用 const
└─ 函数参数、配置对象、DOM 引用
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━2. 最佳实践示例
javascript
// ✓ 好的实践:优先使用 const
function processData(data) {
const config = { timeout: 5000 } // 配置不变
let result = [] // 需要累加
for (let i = 0; i < data.length; i++) { // 计数器
const item = data[i] // 循环内不变
result.push(transform(item))
}
return result
}
// ✓ 好的实践:解构赋值用 const
const { name, age } = user
const [first, second] = array
// ✓ 好的实践:函数参数
function createUser({ name, email }) {
// name 和 email 不应该被修改
return { name, email }
}
// ✗ 避免:滥用 let
let config = { timeout: 5000 }
config = { timeout: 10000 } // 为什么不直接用 const?
// ✓ 更好
const config = { timeout: 5000 }
// 如果需要新配置,创建新对象
const newConfig = { ...config, timeout: 10000 }3. 循环中的选择
javascript
// ✓ for 循环:计数器用 let
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
// ✓ for...of:推荐
for (const item of arr) {
console.log(item)
}
// ✓ for...in:不推荐用于数组(会遍历原型链)
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key, obj[key])
}
}
// ✗ while 循环:通常用 let
let count = 0
while (count < 10) {
console.log(count++)
}六、面试标准回答
let 和 const 是 ES6 引入的新声明方式,解决了 var 的多个问题。
主要区别:
- 作用域:var 是函数作用域,let/const 是块级作用域({} 内有效)
- 变量提升:var 会提升到函数顶部,let/const 存在暂时性死区(TDZ),声明前无法访问
- 重复声明:var 允许,let/const 不允许
- 全局挂载:var 声明的全局变量会挂载到 window,let/const 不会
**暂时性死区(TDZ)**是指从进入作用域到变量声明处的区域,在这个区域内访问变量会报 ReferenceError。即使是 typeof 也会报错。
const 的本质是保证绑定关系不变,而非值不变。对于基本类型,值不能改变;对于引用类型,内存地址不能变,但属性可以修改。如果需要完全不可变,可以使用 Object.freeze() 深度冻结。
实际使用中,我遵循以下原则:
- 优先使用 const(约占 90%)
- 需要重新赋值时使用 let(如循环计数器、累加器)
- 不再使用 var
经典应用是解决 setTimeout 循环问题:使用 let 声明循环变量,每次循环都会创建新的绑定,从而正确捕获当前值。
七、记忆口诀
let const 歌诀:
var 提升有问题,
let const 来解决。
块级作用域更安全,
暂时死区要牢记。
const 绑定不改变,
引用类型属性变。
优先 const 少用 let,
代码质量高一级!八、推荐资源
九、总结一句话
- let: 块级作用域 + 可重新赋值 = var 的现代替代 ✓
- const: 常量绑定 + 引用不变 = 默认首选 🎯
- TDZ: 声明前禁访问 = 避免提前使用错误 ⚠️